/*
 *  Copyright (C) 2010 Glauco
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.balisunrise.bindings;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import org.balisunrise.annotation.util.ReflectUtilities;
import org.balisunrise.common.BindingException;

/**
 * Escritor de propriedade de um objeto.
 *
 * @author Glauco
 */
public class BeansPropertyWriter {

    /**
     * Instâncias de escritores de propriedade
     */
    private static Map<Class,BeansPropertyWriter> instances =
            new HashMap<Class, BeansPropertyWriter>();

    /**
     * Classe do objeto que deve ser escrita as propriedades.
     */
    private Class objectClass;

    /**
     * Util para pegar os métodos.
     */
    private ReflectUtilities ru;

    /**
     * Métodos Set das propriedades.
     */
    private Map<String,Method> methods;

    /**
     * Construtor privado.<BR>
     * As instâncias são pegas pelo método {@code getInstance}
     * @see #getInstance(java.lang.Object)
     */
    private BeansPropertyWriter(Class objectClass) {
        this.objectClass = objectClass;
        ru = new ReflectUtilities(objectClass);
        methods = new HashMap<String, Method>();
    }

    /**
     * Pega uma instância de Escritor de propriedade para um determinado
     * objeto.<BR>
     * Se o Objeto for do tipo Class, é utilizado este tipo de dado para fazer
     * a escrita das propriedades, caso contrario, é pego o tipo de dado do
     * objeto.
     *
     * @param o Objeto, o qual deve ser feita a escrita dos dados.
     */
    private static BeansPropertyWriter getInstance(Object o){

        if (o == null)
            throw new NullPointerException(
                    "BeansPropertyWriter#getInstance received a null parm.");

        // pega a classe
        Class clazz;
        if (o instanceof Class )
            clazz = (Class) o;
        else
            clazz = o.getClass();

        // verifica se já exite uma instância.
        if(instances.containsKey(clazz))
            return instances.get(clazz);

        // Cria nova instância
        BeansPropertyWriter instance = new BeansPropertyWriter(clazz);
        instances.put(clazz, instance);

        return instance;
    }

    /**
     * Retorna o Método Set de uma propriedade.
     * @param property propriedade.
     * @return Method.
     */
    private Method getMethod(String property){

        if(methods.containsKey(property))
            return methods.get(property);

        Method m = ru.getSetMethod(property);

        if(m == null)
            throw new NullPointerException(
                    "BeansPropertyWriter can't set a property: " +
                    objectClass.getName() + " don't have a Set Method for \"" +
                    property + "\"");

        methods.put(property, m);

        return m;
    }

    /**
     * Escreve um valor para uma propriedade do source.
     * @param source Objeto com a propriedade que deve receber o valor.
     * @param property Nome da propriedade.
     * @param value Valor que deve ser atribuido a propriedade.
     */
    public static void setProperty(Object source, String property, Object value)
            throws IllegalAccessException, IllegalArgumentException,
            InvocationTargetException, PropertyWriterException,
            BindingException{

        String[] props;
        if(property.contains("."))
            props = property.split("\\.");
        else
            props = new String[]{property};

        int lastIndex = props.length - 1;

        String prop = property;
        Object trueSource = source;

        for (int i = 0; i <= lastIndex; i++) {
            prop = props[i];
            if (i < lastIndex){
                try{
                    trueSource = BeansPropertyReader
                            .getProperty(trueSource,prop);
                }catch(BeansPropertyReader.PropertyReaderException ex){
                    throw new PropertyWriterException(ex);
                }
                if(trueSource == null)
                    throw new PropertyWriterException(null);
            }
        }

        BeansPropertyWriter bpw = getInstance(trueSource);
        Method m = bpw.getMethod(prop);

        m.invoke(trueSource, value);
    }

    /**
     * Exception for null source.
     */
    public static class PropertyWriterException extends Exception{
        public PropertyWriterException(Throwable cause) {
            super("A null source founded.", cause);
        }
    }
}
